Skip to content

feat(component): Adds SmartScrollbar to ui-next - OHIF-2558#5924

Open
dan-rukas wants to merge 20 commits intoOHIF:masterfrom
dan-rukas:feat/smart-scrollbar
Open

feat(component): Adds SmartScrollbar to ui-next - OHIF-2558#5924
dan-rukas wants to merge 20 commits intoOHIF:masterfrom
dan-rukas:feat/smart-scrollbar

Conversation

@dan-rukas
Copy link
Copy Markdown
Member

@dan-rukas dan-rukas commented Mar 26, 2026

Context

This PR adds a SmartScrollbar component to @ohif/ui-next that visualizes slice loading progress, viewed state, and scroll position in viewports.

The component is pure UI — it has no Cornerstone or viewer dependencies. It accepts a slice index, total count, loading state, and Sets of loaded/viewed indices, and handles all measurement, animation, pointer interaction, and visual state internally.

Preview the component, animations, and behaviors here: https://ohif-scroll-design-final.netlify.app/

SmartScrollbar-visual-states

Changes & Results

New files — 7 component files in platform/ui-next/src/components/SmartScrollbar/:

File Role
SmartScrollbar.tsx Root — context provider, ResizeObserver, pointer handling, contraction state
SmartScrollbarTrack.tsx Dot-grid background, visible during loading
SmartScrollbarFill.tsx Colored bars for contiguous slice runs (loaded and viewed)
SmartScrollbarIndicator.tsx Pill-shaped position marker
SmartScrollbarEndpoints.tsx Endcap markers at loaded range boundaries
utils.ts Contiguous run detection, indicator layout math
index.ts Barrel export

Modifiedplatform/ui-next/src/components/index.ts to export the new components.

Usage:

<SmartScrollbar value={index} totalSlices={295} onValueChange={setIndex} isLoading={loading}>
  <SmartScrollbarTrack>
    <SmartScrollbarFill slices={loadedSet} className="bg-neutral/25" loadingClassName="bg-neutral/50" />
    <SmartScrollbarFill slices={viewedSet} className="bg-primary/35" />
  </SmartScrollbarTrack>
  <SmartScrollbarIndicator />
  <SmartScrollbarEndpoints slices={loadedSet} />
</SmartScrollbar>

Key behaviors:

  • Track contracts from 8px to 4px after loading completes (600ms settle delay, 300ms ease transition)
  • Expands on hover or drag
  • Dot-grid pattern fades in during loading, fades out on completion
  • Loaded fill brightens during loading (50% opacity), dims after (25%)
  • Endpoint caps stay stationary during contraction (rendered via portal to a stable layer)
  • Click-to-jump and drag-to-scrub with pointer capture
  • Error thrown if SmartScrollbarIndicator is omitted
  • Uses existing OHIF design tokens (--neutral, --primary, --foreground)
  • Fill colors are consumer-configurable via Tailwind class props

Testing

No Testing Needed.

This PR adds a UI component only — no viewer integration is included. The component can be previewed and tested interactively here:

Live demo: https://ohif-scroll-design-final.netlify.app/

  • Press Play to see the loading lifecycle (dot-grid, fills, contraction)
  • Scroll the viewport to move the indicator
  • Hover the contracted scrollbar to see it expand
  • Drag the indicator to scrub through slices

Checklist

PR

  • My Pull Request title is descriptive, accurate and follows the
    semantic-release format and guidelines.

Code

  • My code has been well-documented (function documentation, inline comments,
    etc.)

Public Documentation Updates

  • The documentation page has been updated as necessary for any public API
    additions or removals.

Tested Environment

  • OS: macOS Sequoia 15.7.4 (24G517), Apple M4 Pro
  • Node version: 20.19.1
  • Browser: Google Chrome (latest) 146.0.7680.165 (Official Build) (arm64)

Greptile Summary

This PR introduces SmartScrollbar, a pure-UI compound component for @ohif/ui-next that visualises slice loading progress, viewed state, and current scroll position in viewports. The implementation is self-contained with no Cornerstone or viewer dependencies, accepting a value, total, onValueChange, and isLoading prop alongside Uint8Array-backed fill/endpoint children.

Key design choices and outcomes:

  • Pixel-space rendering via computePixelFilledFromMarked prevents fills from overstating coverage when many slice indices map to a single pixel row.
  • useByteArray hook manages the mutable Uint8Array + incrementing version pattern so fill components re-render correctly even when the array is mutated in-place; an optional debounceMs batches high-frequency cache events.
  • Stable layer + createPortal ensures SmartScrollbarEndpoints caps don't shift during the track contraction animation; the previously flagged ref-timing bug is fixed with a useState callback ref.
  • Opt-in keyboard navigation (enableKeyboardNavigation, default false) avoids conflicts with OHIF's existing keyboard shortcuts while keeping WAI-ARIA slider compliance available.
  • ByteArrayHandle and SmartScrollbarLayoutContextValue types are correctly exported from the inner barrel (SmartScrollbar/index.ts) but are not forwarded from the top-level src/components/index.ts, so consumers of @ohif/ui-next cannot import these types from the package root without deep-path imports.

Confidence Score: 5/5

  • Safe to merge — all prior P1 concerns are resolved; the only remaining finding is a minor missing type re-export at the package entry point.
  • All previously flagged issues (stale fills, endpoint ref timing, unconditional keyboard event capture, dead svgIdPrefix, run-length overflow) have been addressed. The sole remaining finding is a P2 omission: ByteArrayHandle and SmartScrollbarLayoutContextValue are not re-exported from the top-level src/components/index.ts, limiting type discoverability for package consumers. This does not affect runtime behaviour and does not block merge.
  • platform/ui-next/src/components/index.ts — missing type re-exports for ByteArrayHandle and SmartScrollbarLayoutContextValue.

Important Files Changed

Filename Overview
platform/ui-next/src/components/SmartScrollbar/SmartScrollbar.tsx Root compound component; properly uses callback-ref pattern for stable layer, opt-in keyboard navigation, and ResizeObserver for track height measurement. All previously flagged issues (ref timing, keyboard conflicts, dead svgIdPrefix) have been resolved.
platform/ui-next/src/components/SmartScrollbar/useByteArray.ts Clean hook managing a mutable Uint8Array with version-based invalidation and optional debouncing. Guards against out-of-bounds and double-set/double-clear operations; resets correctly on size change.
platform/ui-next/src/components/SmartScrollbar/SmartScrollbarFill.tsx Renders contiguous fill bars in pixel space to avoid subpixel overstatement; uses version prop for memo invalidation alongside the stable Uint8Array reference, addressing the previously identified stale-fill issue.
platform/ui-next/src/components/SmartScrollbar/SmartScrollbarEndpoints.tsx Portals SVG endpoint caps into the stable layer so they don't shift during contraction; correctly returns null when stableLayerEl is not yet mounted, which is now populated immediately via the callback-ref fix.
platform/ui-next/src/components/SmartScrollbar/utils.ts Three pure functions — contiguous run detection, pixel-space fill mapping (conservative downsample + upsample), and indicator layout math — all O(n) with no heap allocations in loops. Run-length clamping addressed in prior review cycles.
platform/ui-next/src/components/SmartScrollbar/index.ts Barrel exports all components, hooks, and types correctly; however, ByteArrayHandle and SmartScrollbarLayoutContextValue types are not forwarded from the top-level package entry (src/components/index.ts).
platform/ui-next/src/components/index.ts Re-exports all runtime symbols for SmartScrollbar correctly, but omits the ByteArrayHandle and SmartScrollbarLayoutContextValue type exports that are available in the inner barrel file.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    SSB["SmartScrollbar\n(root — context provider,\nResizeObserver, pointer & keyboard handling)"]

    LC["SmartScrollbarLayoutContext\n(total, trackHeight, isLoading,\neffectiveWidth, trackWidth,\nfillPadding, stableLayerEl)"]
    SC["SmartScrollbarScrollContext\n(value: number)"]

    SSB --> LC
    SSB --> SC

    SST["SmartScrollbarTrack\n(dot-grid background,\nfades in/out with isLoading)"]
    SSF["SmartScrollbarFill\n(colored bars for contiguous runs;\nmarked: Uint8Array + version)"]
    SSI["SmartScrollbarIndicator\n(pill-shaped position marker;\nrequired child)"]
    SSE["SmartScrollbarEndpoints\n(SVG caps at loaded range boundaries;\nportaled into stable layer)"]

    LC --> SST
    LC --> SSF
    LC --> SSI
    LC --> SSE
    SC --> SSI

    UBA["useByteArray(size, debounceMs?)\n→ ByteArrayHandle\n(bytes, version, isFull,\nsetByte, clearByte, resetWith)"]

    UBA -->|"marked + version"| SSF
    UBA -->|"marked + version"| SSE

    UTILS["utils.ts\ncomputePixelFilledFromMarked()\ncomputeContiguousRuns()\ngetIndicatorLayout()"]

    SSF --> UTILS
    SSE --> UTILS
    SSI --> UTILS
    SSB --> UTILS

    SL["Stable layer div\n(absolute, full size,\npointerEvents: none)"]
    SSB --> SL
    SSE -->|"createPortal"| SL
Loading

Reviews (4): Last reviewed commit: "Add unit tests for SmartScrollbar utils ..." | Re-trigger Greptile

@netlify
Copy link
Copy Markdown

netlify bot commented Mar 26, 2026

Deploy Preview for ohif-dev canceled.

Name Link
🔨 Latest commit a62f792
🔍 Latest deploy log https://app.netlify.com/projects/ohif-dev/deploys/69cd6ba8d8c9800008360d1b

@dan-rukas dan-rukas requested a review from jbocce March 26, 2026 19:42
@jbocce jbocce changed the title feat(component): Adds SmartScrollbar to ui-next feat(component): Adds SmartScrollbar to ui-next - OHIF-2558 Mar 27, 2026
… unnecessary re-renders.

Memoize SmartScrollbar components to prevent unnecessary re-renders.
@jbocce
Copy link
Copy Markdown
Collaborator

jbocce commented Mar 31, 2026

The SmartScrollbar component is supposed to be somewhat generic. Eliminating references to slices would be prudent. I will push a commit to address this soon.

@jbocce
Copy link
Copy Markdown
Collaborator

jbocce commented Apr 1, 2026

The SmartScrollbar component is supposed to be somewhat generic. Eliminating references to slices would be prudent. I will push a commit to address this soon.

Commit hash a388084 addressed this.

@jbocce
Copy link
Copy Markdown
Collaborator

jbocce commented Apr 1, 2026

@greptileai I just pushed bc80fae that addresses a concern we had for ensuring the fills accurately indicate marked items without over and/or under stating items that are marked. This is especially important considering that one fill is used to indicate viewed (and unviewed) medical images. Please have a look and let me know what you think. Thanks.

@jbocce
Copy link
Copy Markdown
Collaborator

jbocce commented Apr 1, 2026

@greptileai I just pushed some unit tests for some of the SmartSrollbar utilities. Please have a look and let me know if there are any you think I should add. Thanks.

@dan-rukas
Copy link
Copy Markdown
Member Author

Tested the latest code and changes and everything looks great and matches the original design and behaviors. Thanks again @jbocce for your help, changes, and detailed approach to getting this right.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants